package org.eenie.hookui.hook


import android.app.Application

import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.android.utils.FlipperUtils
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.soloader.SoLoader
import de.robv.android.xposed.*
import de.robv.android.xposed.callbacks.XC_LoadPackage
import org.eenie.hookui.utils.ProcessUtils
import com.facebook.flipper.plugins.network.NetworkReporter
import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo
import okhttp3.*
import okio.Buffer
import org.eenie.hookui.OkHttpConfig
import org.eenie.hookui.utils.OkHttpUtils
import java.io.IOException
import java.lang.reflect.Method
import java.lang.reflect.Modifier


class HookZiRoomEntry : IXposedHookLoadPackage, IXposedHookZygoteInit {
    private var plugin_path = ""
    private lateinit var clzLoader: ClassLoader
    private var config = OkHttpConfig()
    private var pkgs = arrayListOf("com.ziroom.android.manager")


    override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
        XposedBridge.log("hook start " + lpparam.packageName)
//        if (pkgs.contains(lpparam.packageName)) {
        clzLoader = lpparam.classLoader
        XposedHelpers.findAndHookMethod(
            clzLoader.loadClass("android.app.Application"),
            "onCreate",
            object : XC_MethodHook() {
                override fun beforeHookedMethod(param: MethodHookParam) {

                    val app = param.thisObject as Application
                    XposedBridge.log("app start " + app.packageName)
//                    XposedHelpers.callMethod(
//                        app.classLoader, "addDexPath", arrayOf<Class<*>>(
//                            String::class.java
//                        ), plugin_path
//                    )


                    if (ProcessUtils.isMainProcess(app)) {
                        val clientClzName = OkHttpUtils.getAllClassName(clzLoader).find {
                            return@find clzLoader.loadClass(it).interfaces.size == 3
                        }


                        config.OkHttpClientName = clientClzName ?: ""





                        XposedBridge.log("client clz = " + clientClzName)
                        clzLoader.loadClass(clientClzName).methods.find {
                            return@find it.parameterTypes.size == 1 && it.returnType.isInterface
                        }.also {
                            val newCallMethod = it!!
                            val requestClzName = it.parameterTypes[0].name
                            XposedBridge.log("request clz " + requestClzName)

                            val buildClzName = it.parameterTypes[0].classes[0].name
                            val buildClz = loadClass(buildClzName)
                            val buildObj = XposedHelpers.newInstance(buildClz)
                            var realCallClz: Class<*>? = null
                            var builderMethod: Method? = null

                            buildClz?.methods?.forEach {
                                if (it.parameterTypes.size == 1 && it.parameterTypes[0].name == String::class.java.name) {
                                    XposedHelpers.callMethod(buildObj, it.name, "http://www.baidu.com")
                                }

                                if (it.parameterTypes.isEmpty() && it.returnType.name == requestClzName) {
                                    builderMethod = it
                                }
                            }

                            realCallClz = newCallMethod.invoke(XposedHelpers.newInstance(loadClass(clientClzName!!)), builderMethod?.invoke(buildObj))!!::class.java
                            XposedBridge.log("realcall clz " + realCallClz.name)
                            config.RealCallName = realCallClz.name
                            config.RealCall_method_getResponseWithInterceptorChainMethod = ""

                            realCallClz.declaredMethods.forEach {
                                if (it.returnType.name == requestClzName) {
                                    XposedBridge.log("realcall request method " + it.name)
                                    config.RealCall_method_request = it.name
                                } else if (it.exceptionTypes.isNotEmpty() && it.exceptionTypes.size == 1 && it.exceptionTypes[0].name == IOException::class.java.name) {
                                    XposedBridge.log("realcall getResponseWithInterceptorChainMethod method " + it.name)
                                    config.RealCall_method_getResponseWithInterceptorChainMethod = it.name
                                }
                            }


                            loadClass(requestClzName)?.declaredFields?.forEach {
                                if (it.type.name == String::class.java.name) {
                                    XposedBridge.log("method field " + it.name)
                                    config.Request_field_method = it.name
                                    return@forEach
                                }

                                if (it.type.constructors.isNotEmpty() && it.type.constructors.size == 1 && it.type.constructors[0].parameterAnnotations.isEmpty()) {
                                    XposedBridge.log("body field " + it.name)
                                    config.Request_field_body = it.name
                                    return@forEach
                                }

                                if (it.type.declaredFields.size > 18) {
                                    XposedBridge.log("url field " + it.name)
                                    config.Request_field_url = it.name
                                    return@forEach
                                }

                                if (it.type.interfaces.filter { it.name == "java.lang.Iterable" }
                                        .isNotEmpty() || (Modifier.isFinal(it.type.modifiers) && it.type.declaredFields.size == 1 && it.type.declaredFields.first().type.isArray)) {
                                    XposedBridge.log("headers field " + it.name)
                                    config.Request_field_headers = it.name
                                    return@forEach
                                }
                            }


                        }

                        XposedBridge.log(config.toString())

                        injectFlipper(app)
                    }


                }
            })
//        }
    }


    fun injectFlipper(app: Application) {
        SoLoader.init(app, false)
        if (FlipperUtils.shouldEnableFlipper(app)) {
            val client = AndroidFlipperClient.getInstance(app)

            val inspectorPlugin = InspectorFlipperPlugin(app, DescriptorMapping.withDefaults())
            client.addPlugin(inspectorPlugin)
            client.addPlugin(DatabasesFlipperPlugin(app))
            client.addPlugin(NavigationFlipperPlugin.getInstance())
            val networkFlipperPlugin = NetworkFlipperPlugin()

            try {
//                if (loadClass("okhttp3.internal.connection.RealCall") != null) {
//                    XposedHelpers.findAndHookMethod(clzLoader.loadClass("okhttp3.internal.connection.RealCall"),
//                        "getResponseWithInterceptorChain\$okhttp",
//                        object : XC_MethodHook() {
//                            override fun beforeHookedMethod(param: MethodHookParam) {
//
//
//                                val id = param.thisObject.hashCode().toString()
//                                val request = XposedHelpers.callMethod(param.thisObject, "request")
//                                val method = XposedHelpers.callMethod(request, "method").toString()
//                                val contentType = XposedHelpers.callMethod(XposedHelpers.callMethod(request, "body"), "contentType").toString()
//                                val headers = convertHeader(XposedHelpers.getObjectField(request, "headers"))
//                                headers.add(NetworkReporter.Header("Content-Type", contentType))
//                                val url = XposedHelpers.callMethod(request, "url").toString()
//                                val content = cloneBodyAndInvalidateRequest(request)
////                            XposedBridge.log("http request \n $method \n $headers \n $url \n $content")
//                                val info = convertRequest(id, method, url, headers, content)
//                                networkFlipperPlugin.reportRequest(info)
//                            }
//
//                            override fun afterHookedMethod(param: MethodHookParam) {
//                                val response = param.result
//                                val id = param.thisObject.hashCode().toString()
//                                val code = XposedHelpers.callMethod(response, "code").toString().toInt()
//                                val headers = XposedHelpers.callMethod(response, "headers")
//                                val realHeaders = convertHeader(headers)
//
//                                val timestamp = XposedHelpers.callMethod(response, "receivedResponseAtMillis").toString().toLong()
//
//                                val content = cloneBodyForResponse(response, mMaxBodyBytes)
//
////                            XposedBridge.log("\n$id \n$code \n$timestamp \n$mediaType \n$headers \n$content")
//
//                                val responseInfo = convertResponse(id, code, timestamp, realHeaders, content, false)
//                                networkFlipperPlugin.reportResponse(responseInfo)
//                            }
//                        }
//                    )
//                } else if (loadClass("okhttp3.RealCall") != null) {
//                    XposedHelpers.findAndHookMethod(clzLoader.loadClass("okhttp3.RealCall"),
//                        "getResponseWithInterceptorChain",
//                        object : XC_MethodHook() {
//                            override fun beforeHookedMethod(param: MethodHookParam) {
//                                val id = param.thisObject.hashCode().toString()
//                                val request = XposedHelpers.callMethod(param.thisObject, "request")
//                                val method = XposedHelpers.callMethod(request, "method").toString()
//                                val contentType = XposedHelpers.callMethod(XposedHelpers.callMethod(request, "body"), "contentType").toString()
//                                val headers = convertHeader(XposedHelpers.getObjectField(request, "headers"))
//                                headers.add(NetworkReporter.Header("Content-Type", contentType))
//                                val url = XposedHelpers.callMethod(request, "url").toString()
//                                val content = cloneBodyAndInvalidateRequest(request)
////                            XposedBridge.log("http request \n $method \n $headers \n $url \n $content")
//                                val info = convertRequest(id, method, url, headers, content)
//                                networkFlipperPlugin.reportRequest(info)
//                            }
//
//                            override fun afterHookedMethod(param: MethodHookParam) {
//
//                                val response = param.result
//                                val id = param.thisObject.hashCode().toString()
//                                val code = XposedHelpers.callMethod(response, "code").toString().toInt()
//                                val headers = XposedHelpers.callMethod(response, "headers")
//                                val realHeaders = convertHeader(headers)
//                                val timestamp = XposedHelpers.callMethod(response, "receivedResponseAtMillis").toString().toLong()
//                                val content = cloneBodyForResponse(response, mMaxBodyBytes)
////                            XposedBridge.log("\n$id \n$code \n$timestamp \n$mediaType \n$headers \n$content")
//                                val responseInfo = convertResponse(id, code, timestamp, realHeaders, content, false)
//                                networkFlipperPlugin.reportResponse(responseInfo)
//                            }
//                        }
//                    )
//                } else {


                XposedHelpers.findAndHookMethod(loadClass(config.RealCallName),
                    config.RealCall_method_getResponseWithInterceptorChainMethod,
                    object : XC_MethodHook() {
                        override fun beforeHookedMethod(param: MethodHookParam) {
                            val id = param.thisObject.hashCode().toString()

                            val request = XposedHelpers.callMethod(param.thisObject, config.RealCall_method_request)
                            val method = XposedHelpers.callMethod(request, config.Request_field_method).toString()
                            val body = XposedHelpers.getObjectField(request, config.Request_field_body)
                            var contentType = ""
                            if (body != null) {
                                contentType = XposedHelpers.callMethod(body, "contentType").toString()
                            }
                            XposedBridge.log("headers = " + XposedHelpers.callMethod(XposedHelpers.getObjectField(request, config.Request_field_headers),"e"))

//                            val contentType = XposedHelpers.callMethod(, "contentType").toString()
//                            val headers = convertHeader(XposedHelpers.getObjectField(request, config.Request_field_headers))
//                            headers.add(NetworkReporter.Header("Content-Type", "contentType"))
                            val url = XposedHelpers.callMethod(request, config.Request_field_url).toString()
//                            val content = cloneBodyAndInvalidateRequest(request)
                            XposedBridge.log("http request \n $url  \n $method  ")
                            val info = convertRequest(id, method, url, arrayListOf(), ByteArray(0))
                            networkFlipperPlugin.reportRequest(info)
                        }

                        override fun afterHookedMethod(param: MethodHookParam) {
                            val response = param.result
                            val id = param.thisObject.hashCode().toString()
                            val code = XposedHelpers.callMethod(response, "code").toString().toInt()
                            val headers = XposedHelpers.callMethod(response, "headers")
                            val realHeaders = convertHeader(headers)
                            val timestamp = XposedHelpers.callMethod(response, "receivedResponseAtMillis").toString().toLong()
                            val content = cloneBodyForResponse(response, mMaxBodyBytes)
                            XposedBridge.log("\n$id \n$code \n$timestamp  \n$headers \n$content")
                            val responseInfo = convertResponse(id, code, timestamp, realHeaders, content, false)
                            networkFlipperPlugin.reportResponse(responseInfo)
                        }
                    }
                )


//                }


            } catch (e: Exception) {
                XposedBridge.log(e)
            }
            client.addPlugin(networkFlipperPlugin)


            client.start()
        }
    }


    fun loadClass(name: String): Class<*>? {
        try {
            return clzLoader.loadClass(name)
        } catch (e: Exception) {

        }
        return null
    }


    private val mMaxBodyBytes: Long = 1024 * 1024

    @Throws(IOException::class)
    private fun convertRequest(id: String, method: String, url: String, headers: List<NetworkReporter.Header>, content: ByteArray): NetworkReporter.RequestInfo {
        val info = NetworkReporter.RequestInfo()
        info.requestId = id
        info.timeStamp = System.currentTimeMillis()
        info.headers = headers
        info.method = method
        info.uri = url
        info.body = content
        return info
    }

    @Throws(IOException::class)
    private fun cloneBodyAndInvalidateRequest(request: Any): ByteArray {
        val body = XposedHelpers.callMethod(request, "body")
        if (body != null) {
            val originalBuffer = XposedHelpers.newInstance(clzLoader.loadClass("okio.Buffer"))
            XposedHelpers.callMethod(body, "writeTo", originalBuffer)
            val clonedBuffer = XposedHelpers.callMethod(originalBuffer, "clone")
            return XposedHelpers.callMethod(clonedBuffer, "readUtf8").toString().toByteArray()
        }
        return ByteArray(0)
    }


    @Throws(IOException::class)
    private fun bodyBufferToByteArray(bodyBuffer: Buffer, maxBodyBytes: Long): ByteArray? {
        return bodyBuffer.readByteArray(Math.min(bodyBuffer.size, maxBodyBytes))
    }


    private fun convertHeader(headers: Any?): MutableList<NetworkReporter.Header> {
        val realHeaders = mutableListOf<NetworkReporter.Header>()
        if (headers != null) {
            val size = XposedHelpers.callMethod(headers, "size").toString().toInt()
            for (index in 0 until size) {
                val name = XposedHelpers.callMethod(headers, "name", index).toString()
                val value = XposedHelpers.callMethod(headers, "value", index).toString()
                realHeaders.add(NetworkReporter.Header(name, value))
            }
        }
        return realHeaders
    }

    @Throws(IOException::class)
    private fun convertResponse(
        identifier: String,
        code: Int,
        receivedResponseAtMillis: Long,
        headers: List<NetworkReporter.Header>,
        content: ByteArray,
        isMock: Boolean
    ): ResponseInfo? {
        val info = ResponseInfo()
        info.requestId = identifier
        info.timeStamp = receivedResponseAtMillis
        info.statusCode = code
        info.headers = headers
        info.isMock = isMock
        info.body = content
        return info
    }

    @Throws(IOException::class)
    private fun cloneBodyForResponse(response: Any, maxBodyBytes: Long): ByteArray {
        val body = XposedHelpers.callMethod(response, "body")
        if (body != null) {
            val source = XposedHelpers.callMethod(body, "source")
            XposedHelpers.callMethod(source, "request", maxBodyBytes)
            val buffer = XposedHelpers.callMethod(source, "buffer")
            val cloneBuffer = XposedHelpers.callMethod(buffer, "clone")
            return XposedHelpers.callMethod(cloneBuffer, "readUtf8").toString().toByteArray()
        }
        return ByteArray(0)
    }

    override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam?) {
        plugin_path = startupParam?.modulePath ?: ""
        XposedBridge.log(plugin_path)
    }


}